package weeny.recycle;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import prefuse.data.Edge;
import prefuse.data.Graph;
import prefuse.data.Node;
import prefuse.data.Schema;
import prefuse.data.Table;
import prefuse.data.Tuple;
import prefuse.data.io.*;
import prefuse.util.TypeLib;
import prefuse.util.io.*;

/**
 * <p>ArraySupportedGraphWriter extends GraphMLWriter, by taking int[],
 * float[], and double[] as table fields. And automatically convert
 * ? extends Collection into arrays, AS LONG AS its elements are primitive.</p>
 * <p>
 * It also only has a constructor with node table schema, and edge table
 * schema. By providing these two schemas, this class can only output
 * the fields in them. If these two schemas are not provided, then all
 * columns will be outputed. The order to decide the columes type is:
 * schema type > table type > replace type (which is set by function SetBackupType). 
 * </p>
 * <p>
 * If a cell is null, then it will not be written to the file, in case the table is
 * large and sparse.
 * </p>
 * 
 */
@Deprecated
public class ArraySupportedGraphWriter extends AbstractGraphWriter {

    /**
     * String tokens used in the GraphML format.
     */
    public interface Tokens extends GraphMLReader.Tokens  {
        public static final String GRAPHML = "graphml";
        
        public static final String GRAPHML_HEADER =
            "<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\"\n" 
            +"  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
            +"  xsi:schemaLocation=\"http://graphml.graphdrawing.org/xmlns\n"
            +"  http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd\">\n\n";
        public static final String INT_ARRAY     = "intarray";
        public static final String FLOAT_ARRAY   = "floatarray";
        public static final String DOUBLE_ARRAY  = "doublearray";
        public static final String BOOLEAN_ARRAY = "booleanarray";
        public static final String LONG_ARRAY    = "longarray";
        public static final String STRING_ARRAY  = "stringarray";
    }
    
	protected Schema m_NodeSchemaToWrite = null;

	protected Schema m_EdgeSchemaToWrite = null;
	
	public static String makeCSVSafe(String s) {
        int q = -1;
        if ( (q=s.indexOf('\"')) >= 0 ||
             s.indexOf(',')  >= 0 || s.indexOf('\n') >= 0 ||
             Character.isWhitespace(s.charAt(0)) ||
             Character.isWhitespace(s.charAt(s.length()-1)) )
        {
            if ( q >= 0 ) s = s.replaceAll("\"", "\"\"");
            s = "\""+s+"\"";
        }
        return s;
    }
	public static String csvString(String[] strings){
		if (strings == null) {
			return null;
		}
		if (strings.length == 0) {
			return "";
		}
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < strings.length; i++) {
			if (i>0) 
				sb.append(",");
			if(strings[i].length()>0)
				sb.append(makeCSVSafe(strings[i]));
		}
		return sb.toString();
	}
	public static String csvString(Collection coll){
		if (coll == null) {
			return null;
		}
		String[] strings = new String[coll.size()];
		int idx = 0;
		for(Object o : coll){
			strings[idx++] = o.toString();
		}
		return csvString(strings);
	}
    /**
     * Map containing legal data types and their names in the GraphML spec
     */
    public static final HashMap TYPES = new HashMap();
    static {
        TYPES.put(int.class,       Tokens.INT);
        TYPES.put(long.class,      Tokens.LONG);
        TYPES.put(float.class,     Tokens.FLOAT);
        TYPES.put(double.class,    Tokens.DOUBLE);
        TYPES.put(boolean.class,   Tokens.BOOLEAN);
        TYPES.put(String.class,    Tokens.STRING);
        TYPES.put(int[].class,     Tokens.INT_ARRAY);
        TYPES.put(float[].class,   Tokens.FLOAT_ARRAY);
        TYPES.put(double[].class,  Tokens.DOUBLE_ARRAY);
        TYPES.put(boolean[].class, Tokens.BOOLEAN_ARRAY);
        TYPES.put(long[].class,    Tokens.LONG_ARRAY);
        TYPES.put(String[].class,  Tokens.STRING_ARRAY);
    }
    
    public ArraySupportedGraphWriter(){
    	m_NodeSchemaToWrite = null;
    	m_EdgeSchemaToWrite = null;
    }
    protected Map<String, Class> replace = new TreeMap<String, Class>(); 
    public void setBackupType(String column, Class type){
    	if (!TYPES.containsKey(type)) {
			throw new IllegalArgumentException("type: "+type+" is not supported");
		}
    	replace.put(column, type);
    }
    public ArraySupportedGraphWriter(Schema nSchemaToWrite){
    	m_NodeSchemaToWrite = nSchemaToWrite;
    }
	public ArraySupportedGraphWriter(Schema nSchemaToWrite, Schema eSchemaToWrite) {
		m_EdgeSchemaToWrite = eSchemaToWrite;
		m_NodeSchemaToWrite = nSchemaToWrite;
	}
	
	protected Class getArrayClassForCollection(Table table, String field){
		for(Iterator<Tuple> it = table.tuples(); it.hasNext();){
			Collection coll = (Collection) it.next().get(field);
			if (coll == null || coll.isEmpty()) {
				continue;
			}
			else{
				Object elem = coll.iterator().next();
				if (elem instanceof Number) {
					return Array.newInstance(TypeLib.getPrimitiveType(elem.getClass()), 1).getClass();
				}
				else if (elem instanceof String){
					return String[].class;
				}
				else{
					throw new IllegalArgumentException("cannot deal with this elem type: "+elem.getClass()); 
				}
			}
		}
		if (!replace.containsKey(field)) {
			throw new IllegalStateException("cannot find the element type, please specify one by setBackupType()");
		}
		return replace.get(field);
	}
	
	protected Schema convertSchema(Schema schema, Table table){
		Schema resultSchema = new Schema();
		for(int i = 0; i<schema.getColumnCount(); i++){
			String field = schema.getColumnName(i);
			if (table.getColumn(field) == null) {
				throw new IllegalArgumentException("schema is bigger than table");
			}
			else if (TYPES.containsKey(schema.getColumnType(field))) {
				//if the type in schema is ok
				resultSchema.addColumn(field, schema.getColumnType(field));
			}
			else if (TYPES.containsKey(table.getColumnType(field))){
				//if the type in the table is ok
				resultSchema.addColumn(field, table.getColumnType(field));
			}
			else if (Collection.class.isAssignableFrom(table.getColumnType(field))) {
				//if the type in the table can be converted to array
				resultSchema.addColumn(field, getArrayClassForCollection(table, field));
			}
			else{
				throw new IllegalStateException("not a good field (e.g., primative type or collection)");
			}
		}
		return resultSchema;
	}
	
    /**
     * @see prefuse.data.io.GraphWriter#writeGraph(prefuse.data.Graph, java.io.OutputStream)
     */
    public void writeGraph(Graph graph, OutputStream os) throws DataIOException
    {
		Schema ns = graph.getNodeTable().getSchema();
		Schema es = graph.getEdgeTable().getSchema();
		if (m_NodeSchemaToWrite == null) {
			m_NodeSchemaToWrite = new Schema();
			for(int i = 0; i<ns.getColumnCount(); i++){
				m_NodeSchemaToWrite.addColumn(ns.getColumnName(i), ns.getColumnType(i));
			}
		}
		if (m_EdgeSchemaToWrite == null) {
			m_EdgeSchemaToWrite = new Schema();
			for(int i = 0; i<es.getColumnCount(); i++){
				m_EdgeSchemaToWrite.addColumn(es.getColumnName(i), es.getColumnType(i));
			}
		}

		m_NodeSchemaToWrite = convertSchema(m_NodeSchemaToWrite, graph.getNodeTable());
		m_EdgeSchemaToWrite = convertSchema(m_EdgeSchemaToWrite, graph.getEdgeTable());
		
        // first, check the schemas to ensure GraphML compatibility
        checkGraphMLSchema(m_NodeSchemaToWrite);
        checkGraphMLSchema(m_EdgeSchemaToWrite);
        
        XMLWriter xml = new XMLWriter(new PrintWriter(os));
        xml.begin(Tokens.GRAPHML_HEADER, 2);
        
        xml.comment("Generated at " + new Date(System.currentTimeMillis()));
        
        // print the graph schema
        printSchema(xml, Tokens.NODE, m_NodeSchemaToWrite, null);
        printSchema(xml, Tokens.EDGE, m_EdgeSchemaToWrite, new String[] {
            graph.getEdgeSourceField(), graph.getEdgeTargetField()
        });
        xml.println();
        
        // print graph contents
        xml.start(Tokens.GRAPH, Tokens.EDGEDEF,
            graph.isDirected() ? Tokens.DIRECTED : Tokens.UNDIRECTED);
        
        // print the nodes
        xml.comment("nodes");
        Iterator nodes = graph.nodes();
        while ( nodes.hasNext() ) {
            Node n = (Node)nodes.next();
            
            if ( m_NodeSchemaToWrite.getColumnCount() > 0 ) {
                xml.start(Tokens.NODE, Tokens.ID, String.valueOf(n.getRow()));
                for ( int i=0; i<m_NodeSchemaToWrite.getColumnCount(); ++i ) {
                    String field = m_NodeSchemaToWrite.getColumnName(i);
                    String content = content(n,field);
                    if(content.equals("null"))
                    	continue;
                    xml.contentTag(Tokens.DATA, Tokens.KEY, field, content);
                }
                xml.end();
            } else {
                xml.tag(Tokens.NODE, Tokens.ID, String.valueOf(n.getRow()));
            }
        }
        
        // add a blank line
        xml.println();
        
        // print the edges
        String[] attr = new String[]{Tokens.ID, Tokens.SOURCE, Tokens.TARGET};
        String[] vals = new String[3];
        
        xml.comment("edges");
        Iterator edges = graph.edges();
        while ( edges.hasNext() ) {
            Edge e = (Edge)edges.next();
            vals[0] = String.valueOf(e.getRow());
            vals[1] = String.valueOf(e.getSourceNode().getRow());
            vals[2] = String.valueOf(e.getTargetNode().getRow());
            
            if ( es.getColumnCount() > 2 ) {
                xml.start(Tokens.EDGE, attr, vals, 3);
                for ( int i=0; i<m_EdgeSchemaToWrite.getColumnCount(); ++i ) {
                    String field = m_EdgeSchemaToWrite.getColumnName(i);
                    if ( field.equals(graph.getEdgeSourceField()) ||
                         field.equals(graph.getEdgeTargetField()) )
                        continue;
                    String content = content(e,field);
                    if (content.equals("null"))
						continue;
                    xml.contentTag(Tokens.DATA, Tokens.KEY, field, content);
                }
                xml.end();
            } else {
                xml.tag(Tokens.EDGE, attr, vals, 3);
            }
        }
        xml.end();
        
        // finish writing file
        xml.finish("</"+Tokens.GRAPHML+">\n");
        try {
			os.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
    }
    DecimalFormat integerFormat = new DecimalFormat("#");
    DecimalFormat floatFormat = new DecimalFormat("#.###");
    DecimalFormat doubleFormat = new DecimalFormat("#.######");
    FieldPosition fp = new FieldPosition(0);
    protected String content(Tuple n, String field){
    	StringBuffer sb = new StringBuffer();
    	if (n.get(field)==null) {
			return "null";
		}
    	if (n.canGet(field, Collection.class)){
    		Collection collection = (Collection) n.get(field);
    		if (collection.isEmpty()) {
				return "";
			}
    		Object example = collection.iterator().next();
    		if (example.getClass().equals(String.class)) {
				sb.append(csvString(collection));
				return sb.toString();
			}
    		if (example.getClass() == Boolean.class || example.getClass() == Long.class ||
    			example.getClass() == Integer.class ) {
				for(Object object : collection){
					sb.append(object+" ");
				}
				return sb.substring(0, sb.length()-1);
			}
    		
    		DecimalFormat formater = null;
    		if(example.getClass() == Float.class){
    			formater = floatFormat;
    		}
    		else if(example.getClass() == Double.class){
    			formater = floatFormat;
    		}
    		else{
    			throw new IllegalArgumentException("the collection should contains only the primitive wrapped types");
    		}
    		for (Object object : collection) {
				formater.format(object, sb, fp);
				sb.append(" ");
			}
    		return sb.substring(0, sb.length()-1);
    	}
    	if (TYPES.get(n.getColumnType(field)) == null) {
			throw new IllegalArgumentException("if it gets here, type should be in TYPES.");
		}
    	try {
    		Object array = n.get(field);
    		if (array.getClass() == String[].class) {
				sb.append(csvString((String[]) array));
			}
    		else{
    			int size = Array.getLength(array);
    			for(int i = 0; i<size; i++){
    				if (i > 0) sb.append(" ");
    				sb.append(Array.get(array, i).toString());
    			}
    		}
			return sb.toString();
		} catch (IllegalArgumentException e) {
			return n.getString(field);
			//if it is not a array, should be primitive
		}
    }

    /**
     * Print a table schema to a GraphML file
     * @param xml the XMLWriter to write to
     * @param group the data group (node or edge) for the schema
     * @param s the schema
     */
    protected void printSchema(XMLWriter xml, String group, Schema s,
                             String[] ignore)
    {
        String[] attr = new String[] {Tokens.ID, Tokens.FOR,
                Tokens.ATTRNAME, Tokens.ATTRTYPE };
        String[] vals = new String[4];

OUTER:
        for ( int i=0; i<s.getColumnCount(); ++i ) {
            vals[0] = s.getColumnName(i);
            
            for ( int j=0; ignore!=null && j<ignore.length; ++j ) {
                if ( vals[0].equals(ignore[j]) )
                    continue OUTER;
            }
            
            vals[1] = group;
            vals[2] = vals[0];
            vals[3] = (String)TYPES.get(s.getColumnType(i));
            if (vals[3] == null) {
				vals[3] = (String) TYPES.get(replace.get(s.getColumnName(i)));
			}
            Object dflt = s.getDefault(i);
            
            if ( dflt == null ) {
                xml.tag(Tokens.KEY, attr, vals, 4);
            } else {
                xml.start(Tokens.KEY, attr, vals, 4);
                xml.contentTag(Tokens.DEFAULT, dflt.toString());
                xml.end();
            }
        }
    }
    
    /**
     * Checks if all Schema types are compatible with the GraphML specification.
     * The GraphML spec only allows the types <code>int</code>,
     * <code>long</code>, <code>float</code>, <code>double</code>,
     * <code>boolean</code>, and <code>string</code>.
     * @param s the Schema to check
     */
    protected void checkGraphMLSchema(Schema s) throws DataIOException {
        for ( int i=0; i<s.getColumnCount(); ++i ) {
            Class type = s.getColumnType(i);
            if ( TYPES.get(type) == null ) {
            	throw new DataIOException("Data type unsupported by the "+ "GraphML format: " + type.getName());
            }
        }
    }
    
} // end of class GraphMLWriter
